package model;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Observer;

/**
 * Objects of this class contain information about a list of drawing objects.  The objects are
 * arranged in sequential order that they should be drawn in.
 */
public class DrawingModel extends Observable implements Observer
{
	protected ArrayList <DrawObject> myObjects;
	protected String fileName;
	
	private boolean changed;
	
	/**
	 * Creates an empty drawing model that has a unique default file name and no contents
	 */
	public DrawingModel ()
	{
		constructMe();
	}
	
	private void constructMe()
	{
		myObjects = new ArrayList<DrawObject>();
		fileName = "";
		changed = true;
	}
	
	/** 
	 * Copy constructor
	 * @param dm the DrawingModel to be copied.
	 */
	public DrawingModel(DrawingModel dm)
	{
		myObjects = copyObjects(dm.myObjects);
		fileName = new String(dm.fileName);
		changed = true;
		
//		for (DrawObject obj : myObjects)
//		{
//			obj.deleteObservers();
//			obj.addObserver(this);
//		}
		
	}
	
	private ArrayList<DrawObject> copyObjects( ArrayList<DrawObject> objects)
	{
		ArrayList<DrawObject> results = new ArrayList<DrawObject>();
		
		for (DrawObject obj : objects)
		{
			DrawObject newObj = convertObject(obj);
			newObj.addObserver(this);
			results.add(newObj);
			
		}
		
		
		return results;
	}
	
	private DrawObject convertObject(DrawObject obj)
	{
		if (obj instanceof DrawLine)
			return new DrawLine(obj);
		
		if (obj instanceof DrawRect)
			return new DrawRect(obj);
		
		if (obj instanceof DrawEllipse)
			return new DrawEllipse(obj);
		
		if (obj instanceof DrawBezier)
			return new DrawBezier(obj);
		
		if (obj instanceof DrawText)
			return new DrawText(obj);
		
		
		
		return null;
	}

	/**
	 * Reads the specific file and creates a new DrawingModel object that contains all of the 
	 * information in the file. If there is no such file then an exception should be thrown.
	 * @param fileName the name of the file to be read.
	 */
	public DrawingModel(String fileName)
	{
		File f = new File(fileName);
		
		if ( !f.exists() )
			throw new NoFileRTE("File does not exist.");
		
		myObjects = new ArrayList<DrawObject>();
		this.fileName = fileName;
		
		loadObjects();
		changed = false;
	}
	
	private void loadObjects()
	{
		try
		{
			BufferedReader input = new BufferedReader( new FileReader(fileName));
			
			String tmpLine = input.readLine();
			while(tmpLine != null)
			{	
				if (tmpLine.equals(""))
				{
					tmpLine = input.readLine();
					continue;
				}
				
				this.addDrawObject( getObject(tmpLine) );		
				tmpLine = input.readLine();
			}
			
			input.close();
		}
		catch (Exception e)
		{
			System.out.println(e);
		}
	}
	

	private DrawObject getObject (String tmpL)
	{
		char type = tmpL.charAt(0);
		
		switch (type)
		{
		case 'L':
			return new DrawLine(tmpL);
		case 'R':
			return new DrawRect(tmpL);
		case 'E':
			return new DrawEllipse(tmpL);
		case 'B':
			return new DrawBezier(tmpL);
		case 'T':
			return new DrawText(tmpL);
		default:
			throw new RuntimeException("That file contains incorrect grammar for this program!");
		}
		
	}
	
	/**
	 * Returns the name of the file associated with this model.
	 */
	public String getFileName()
	{
		return fileName;
	}
	/**
	 * Changes the file name associated with this model
	 * @param newFileName the new file name
	 */
	public void setFileName(String newFileName)
	{
		this.fileName = newFileName;
		changed = true;
	}
	/**
	 * Saves the contents of this model to its file.
	 */
	public void save()
	{
		saveObjects();
		changed = false;
	}
	
	private void saveObjects()
	{
		try
		{
			BufferedWriter output = new BufferedWriter( new FileWriter(fileName) );
			
			String tmp = new String();
			for (int i=0; i<myObjects.size(); i++)
			{
				tmp = myObjects.get(i).toString();
				
				if (i != (myObjects.size() - 1) )
					tmp += "\n";
				
				output.write(  tmp );
			}
			
			output.close();
			
		}
		catch (Exception e)
		{
			System.out.println(e);
		}
	}
	
	/**
	 * Returns true if there are unsaved changes.
	 */
	public boolean unsavedChanges()
	{
		return changed;
	}
	/**
	 * Adds the specified DrawObject to the end of the list of drawable objects
	 * @param newDrawObject
	 */
	public void addDrawObject(DrawObject newDrawObject)
	{
		newDrawObject.addObserver(this);
		myObjects.add(newDrawObject);
		changed = true;
	}
	/**
	 * Returns the number of drawable objects in this model.
	 */
	public int nDrawObjects()
	{
		return myObjects.size();
	}
	/**
	 * Returns the specified DrawObject. Indexes begin at zero.
	 * @param i index of the desired object. Must be less than nDrawObjects()
	 */
	public DrawObject getDrawObject(int i)
	{
		if (i >= myObjects.size() || i < 0)
			throw new RuntimeException("Invalid object index!");
		
//		return new DrawObject( myObjects.get(i) );
		return myObjects.get(i);
	}
	
	public void setDrawObject(int i, DrawObject obj)
	{
		if (i >= myObjects.size() || i < 0)
			throw new RuntimeException("Invalid object index!");
		
		obj.addObserver(this);
		
		myObjects.set(i, obj);
		
		changed = true;
		
		this.setChanged();
		this.notifyObservers();
	}
	
	/**
	 * Removes the specified object from the list of drawable objects.
	 * @param i the index of the object to be removed.
	 */
	public void removeDrawObject(int i)
	{
		if (i >= myObjects.size() || i < 0)
			throw new RuntimeException("Invalid object index!");
		
		myObjects.remove(i);
		changed = true;
		
		this.setChanged();
		this.notifyObservers();
	}

//	@Override
//	public boolean equals(Object obj)
//	{
//		if (this == obj)
//			return true;
//		if (obj == null)
//			return false;
//		if (getClass() != obj.getClass())
//			return false;
//		DrawingModel other = (DrawingModel) obj;
//		if (myObjects == null)
//		{
//			if (other.myObjects != null)
//				return false;
//		} else if (!myObjects.equals(other.myObjects))
//			return false;
//		return true;
//	}
	
//	@Override
//	public void notifyObservers(Object arg1)
//	{
//		this.setChanged();
//		super.notifyObservers(arg1);
//	}
	
	
   @Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((fileName == null) ? 0 : fileName.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		DrawingModel other = (DrawingModel) obj;
		if (fileName == null) {
			if (other.fileName != null)
				return false;
		} else if (!fileName.equals(other.fileName))
			return false;
		return true;
	}

/**
	* This method is a regression test to verify that this class is
	* implemented correctly. It should test all of the methods including
	* the exceptions. It should be completely self checking. This 
	* should write "testing DrawingModel" to System.out before it
	* starts and "DrawingModel OK" to System.out when the test
	* terminates correctly. Nothing else should appear on a correct
	* test. Other messages should report any errors discovered.
	**/
	public static void Test()
	{
		boolean success = true;
		System.out.println("Testing DrawingModel");
		
		
		// checking the DrawingModel constructor
		try
		{
			@SuppressWarnings("unused")
			DrawingModel mod = new DrawingModel("bogusFile.txt");
			success = check(success, false, "Exception not thrown on bogusFile.txt!");
		}
		catch (RuntimeException e)
		{ /* The exception was caught. Good. */	}
		
		// checking empty constructor
		DrawingModel mod0 = new DrawingModel();
		success = check(success, mod0.getFileName().equals(""), "getFileName() after empty constructor" );
		
		// checking object count after load
		TestFile.generate("testInput.txt");
		DrawingModel mod = new DrawingModel("testInput.txt");
		success = check(success, mod.nDrawObjects() == 5, "Object count");
		
		// this big section checks to make sure each object was successfully loaded from file.
		DrawLine tmpL = new DrawLine("L 10 20 30 40 2 0 0 255");
		tmpL.setX1(10);
		tmpL.setY1(20);
		tmpL.setX2(30);
		tmpL.setY2(40);
		tmpL.setThickness(2);
		tmpL.setRed(0);
		tmpL.setGreen(0);
		tmpL.setBlue(255);
		success = check(success, mod.getDrawObject(0).equals(tmpL), "new DrawLine(String)");
		
		DrawRect tmpR = new DrawRect("R 10 20 7 50 0 0 127 0");
		tmpR.setX1(10);
		tmpR.setY1(20);
		tmpR.setWidth(7);
		tmpR.setHeight(50);
		tmpR.setThickness(0);
		tmpR.setRed(0);
		tmpR.setGreen(127);
		tmpR.setBlue(0);
		success = check(success, mod.getDrawObject(1).equals(tmpR), "new DrawRect(String)");
		
		DrawEllipse tmpE = new DrawEllipse("E 60 -20 30 40 3 255 0 0");
		tmpE.setX1(60);
		tmpE.setY1(-20);
		tmpE.setWidth(30);
		tmpE.setHeight(40);
		tmpE.setThickness(3);
		tmpE.setRed(255);
		tmpE.setGreen(0);
		tmpE.setBlue(0);
		success = check(success, mod.getDrawObject(2).equals(tmpE), "new DrawElipse(String)");
		
		DrawBezier tmpB = new DrawBezier("B 60 60 70 70 60 90 60 100 2 0 0 255");
		tmpB.setX1(60);
		tmpB.setY1(60);
		tmpB.setX2(70);
		tmpB.setY2(70);
		tmpB.setX3(60);
		tmpB.setY3(90);
		tmpB.setX4(60);
		tmpB.setY4(100);
		tmpB.setThickness(2);
		tmpB.setRed(0);
		tmpB.setGreen(0);
		tmpB.setBlue(255);
		success = check(success, mod.getDrawObject(3).equals(tmpB), "new DrawBezier(String)");
		
		DrawText tmpT = new DrawText("T 80 40 15 \"This is the text to draw\" 0 255 0");
		tmpT.setX1(80);
		tmpT.setY1(40);
		tmpT.setHeight(15);
		tmpT.setText("This is the text to draw");
		tmpT.setRed(0);
		tmpT.setGreen(255);
		tmpT.setBlue(0);
		success = check(success, mod.getDrawObject(4).equals(tmpT), "new DrawText(String)");
		
		// if we get to this point then the constructors should be fine for the model
		success = check(success, success, "DrawModel Constructor");
		
		mod.setFileName("testOutput.txt");
		success = check(success, mod.unsavedChanges(), "Unsaved changes shoulded exist after fileName change");
		
		// read the output messages.
		mod.removeDrawObject(0);
		success = check(success, mod.unsavedChanges(), "Unsaved changes should exist after object removal");
		success = check(success, mod.nDrawObjects()==4 , "Number of objects incorrect after remove");
		success = check(success, mod.getDrawObject(0).equals(tmpR), "Object index does not accurately reflect changes after remove");
		
		try
		{
			mod.getDrawObject(5);
			success = check(success, false, "Exception not thrown. Index larger than range on getDrawObject.");
		}
		catch (RuntimeException e)
		{	/* The exception was caught. Good. */	}
		
		try
		{
			mod.getDrawObject(-1);
			success = check(success, false, "Exception not thrown. Index smaller than range on getDrawObject.");
		}
		catch (RuntimeException e)
		{	/* The exception was caught. Good. */	}
		
		// testing the save()
		mod.setFileName("testOutput.txt");
		mod.save();
		
		DrawingModel mod2 = new DrawingModel("testOutput.txt");
		
		success = check(success, mod.equals(mod2), "Input and output files do not match after load and save!");
		
		TestFile.remove("testInput.txt");
		TestFile.remove("testOutput.txt");
		
		if (success)
			System.out.println("\nDrawingModel OK");
	}
	
	/**
	 * This is a substitute for  assert(). I wanted the same functionality, but I want the program to continue
	 * after a failure 
	 * @param success this is the return value for the calling method. This value allows for a failed test
	 * to propogate through after subsequent passed tests
	 * @param test the case being tested
	 * @param msg message to be printed out on failure
	 * @return
	 */
	private static boolean check(boolean success, boolean test, String msg)
	{
		if (!test)
		{
			System.out.println("\tFAIL: " + msg);
			return false;
		}
		
		return success;
	}

	@Override
	public void update(Observable o, Object arg)
	{
		changed = true;
		this.setChanged();
		this.notifyObservers();
	}	
	
}